记一次 system() 引起的端口抢占

写在前面

在写代码时,难免需要调用别的命令获取一些信息,这时可以用popen()拿到需要的信息;有时又需要启动其他服务,这时可能会用system()直接调用启动该服务的命令。这次要说的,就是用system()函数调用 daemon 进程可能会引起的问题。

现象描述

最近在项目中加入了一个程序A,监听 5081 端口,用于设备间通信。当收到特定的消息时,就会通过 rsync 将指定设备的一些文件拷贝到本机,再重启一些进程。有时就会发现,在重启这些进程的时候,5081 端口可能就会被 B,C,D 进程占用(不是必现,但是很容易复现,而且一定是这几个进程占用)。开始怀疑可能是进程A挂了,被monitor(系统中一个监控各进程生死的脚本)重启了,但是多次发现后确认进程A自始至终没有挂过。还有一个情况是进程 B,C,D 的代码中,并没有监听 5081 端口,有的是监听别的已指定的端口,有的根本则不需要监听端口。而且当进程 A 被杀掉后 B,C,D 必然会监听 5081 端口。

问题分析

发现问题后,也没什么眉目,和大家讨论了不少,大家觉得最不可思议的就是,A进程既然没有挂,为何B,C,D进程就能抢到A进程已经监听的端口,而且B,C,D进程在代码中并未去监听 5081 端口。后来在走查代码时发现在程序A中用system()调用了一个脚本,而在这个脚本中重启了 B,C,D 进程。这才想到会不会和这个system()有关系。

关于 system()

通过 manpage 我们可以了解system()函数的实现原理:

The system() library function uses fork(2) to create a child process that executes the shell command specified in command using execl(3) as follows:

execl(“/bin/sh”, “sh”, “-c”, command, (char *) 0);

system() returns after the command has been completed.

可见,system()函数首先调用fork()创建出一个子进程,再在子进程中调用execl()执行参数中的命令。而关于execl()函数,我们同样可以通过 manpage 了解:

The exec() family of functions replaces the current process image with a new process image.

execl()函数是属于exec()族的,它会用参数中的新程序替换当前的进程。

关于 fork()

大家都知道,fork()产生的子进程,会拷贝父进程的数据段、堆栈等资源(Copy-On-Write),当然也包括打开的socket,既然今天说的是端口抢占,那么肯定和socket相关。在 manpage 中,我们同样可以看到以下信息:

The child inherits copies of the parent’s set of open file descriptors. Each file descriptor in the child refers to the same open file description(see open(2)) as the corresponding file descriptor in the parent. This means that the two descriptors share open file status flags, current file offset, and signal-driven I/O attributes (see the description of F_SETOWN and F_SETSIG in fcntl(2)).

结论

通过上面关于system()fork()的分析,可以知道,问题产生的原因,就是我在A进程中打开了 5081 端口的 socket,然后在通过system()重启B,C,D进程的时候,B,C,D进程作为A进程的子进程,复制了 5081 端口的 socket,因此可能会发生抢占 5081 端口的情况。

问题复现

写了两个小程序 process_A 和 process_B,在 process_A 中监听了 5081 端口,然后通过system()调用 process_B,这时查看 process_B 如果拥有和 process_A 同样的 socket 就可以验证上述结论。然后再杀掉 process_A 如果此时 process_B 抢占 5081 端口则进一步验证上述结论。

需分别编译成 process_A 和 process_B。

process_A代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <signal.h>
#include <arpa/inet.h>
#include <strings.h>
#define SYNC_SERVER_PORT 5081
static int g_sockfd;
static int init_socket()
{
struct sockaddr_in server;
int sockfd;
sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0) {
printf("Creating socket failed.\n");
return -1;
}
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(SYNC_SERVER_PORT);
server.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sockfd, (struct sockaddr *)&server, sizeof(struct sockaddr))\
== -1) {
printf("Bind error.\n");
goto error;
}
return sockfd;
error:
close(sockfd);
return -1;
}
void process_exit(int signal)
{
printf("process exit.\n");
if(g_sockfd > 0) {
close(g_sockfd);
}
exit(0);
}
int main(int argc, char **argv)
{
g_sockfd = init_socket();
if(g_sockfd < 0) {
printf("init socket failed.\n");
return -1;
}
signal(SIGKILL,&process_exit);
signal(SIGINT,&process_exit);
system("./process_B &");
while(1) {
sleep(10);
}
return 0;
}

process_B代码如下:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv)
{
while(1) {
sleep(10);
}
return 0;
}

验证过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
root@ubuntu:~# ps -ef | grep process
root 1704 1646 0 08:16 pts/8 00:00:00 ./process_A
root 1706 1 0 08:16 pts/8 00:00:00 ./process_B
root 1722 1707 0 08:16 pts/9 00:00:00 grep --color=auto process
root@ubuntu:~# netstat -anp | grep 5081
udp 0 0 0.0.0.0:5081 0.0.0.0:* 1704/process_A
root@ubuntu:~# ls -al /proc/1704/fd
total 0
dr-x------ 2 root root 0 Aug 29 08:16 .
dr-xr-xr-x 9 root root 0 Aug 29 08:16 ..
lrwx------ 1 root root 64 Aug 29 08:16 0 -> /dev/pts/8
lrwx------ 1 root root 64 Aug 29 08:16 1 -> /dev/pts/8
lrwx------ 1 root root 64 Aug 29 08:16 2 -> /dev/pts/8
lrwx------ 1 root root 64 Aug 29 08:16 3 -> socket:[21710]
root@ubuntu:~# ls -al /proc/1706/fd
total 0
dr-x------ 2 root root 0 Aug 29 08:16 .
dr-xr-xr-x 9 root root 0 Aug 29 08:16 ..
lr-x------ 1 root root 64 Aug 29 08:16 0 -> /dev/null
lrwx------ 1 root root 64 Aug 29 08:16 1 -> /dev/pts/8
lrwx------ 1 root root 64 Aug 29 08:16 2 -> /dev/pts/8
lrwx------ 1 root root 64 Aug 29 08:16 3 -> socket:[21710]
root@ubuntu:~# kill -9 1704
root@ubuntu:~# netstat -anp | grep 5081
udp 0 0 0.0.0.0:5081 0.0.0.0:* 1706/process_B
root@ubuntu:~#

解决办法

既然找到了问题的根本原因,那么解决问题的办法,就很多了。我的做法是在收到特定消息后,先fork()出子进程,在子进程中关闭了 5081 的 socket,然后再去做 rsync 以及后续的重启操作,同时在父进程中调用waitpid()接收子进程的返回。

至此,端口抢占的问题得到解决。

写在最后

在调查关于system()的问题时,随手翻了一下APUE,在书中还提到了关于用户权限的一个安全性漏洞。书上的举例是程序X具有s权限,在程序X中用system()调用程序Y,这时在Y中打印出geteuid(),会发现 euid 为 0,也就是说Y从X中继承了s权限。其实继承权限和继承fd,也就是同样的问题,所以解决的办法也是相同的。